package com.wmenjoy.utils.bytecode;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import sun.management.MethodInfo;

import com.sun.org.apache.bcel.internal.classfile.StackMap;

/**
 * <code>method_info</code> structure.
 * 
 * @see javassist.CtMethod#getMethodInfo()
 * @see javassist.CtConstructor#getMethodInfo()
 */
public class JMethodInfo {
	ConstantPool constPool;
	int accessFlags;
	int name;
	String cachedName;
	int descriptor;
	ArrayList attribute; // may be null

	/**
	 * If this value is true, Javassist maintains a <code>StackMap</code>
	 * attribute generated by the <code>preverify</code> tool of J2ME (CLDC).
	 * The initial value of this field is <code>false</code>.
	 */
	public static boolean doPreverify = false;

	/**
	 * The name of constructors: <code>&lt;init&gt</code>.
	 */
	public static final String nameInit = "<init>";

	/**
	 * The name of class initializer (static initializer):
	 * <code>&lt;clinit&gt</code>.
	 */
	public static final String nameClinit = "<clinit>";

	private JMethodInfo(final ConstantPool cp) {
		this.constPool = cp;
		this.attribute = null;
	}

	/**
	 * Constructs a <code>method_info</code> structure. The initial value of
	 * <code>access_flags</code> is zero.
	 * 
	 * @param cp
	 *            a constant pool table
	 * @param methodname
	 *            method name
	 * @param desc
	 *            method descriptor
	 * @see Descriptor
	 */
	public JMethodInfo(final ConstantPool cp, final String methodname,
			final String desc) {
		this(cp);
		this.accessFlags = 0;
		this.name = cp.addUtf8Info(methodname);
		this.cachedName = methodname;
		this.descriptor = this.constPool.addUtf8Info(desc);
	}

	JMethodInfo(final ConstantPool cp, final DataInputStream in)
			throws IOException {
		this(cp);
		this.read(in);
	}

	/**
	 * Constructs a copy of <code>method_info</code> structure. Class names
	 * appearing in the source <code>method_info</code> are renamed according to
	 * <code>classnameMap</code>.
	 * 
	 * <p>
	 * Note: only <code>Code</code> and <code>Exceptions</code> attributes are
	 * copied from the source. The other attributes are ignored.
	 * 
	 * @param cp
	 *            a constant pool table
	 * @param methodname
	 *            a method name
	 * @param src
	 *            a source <code>method_info</code>
	 * @param classnameMap
	 *            specifies pairs of replaced and substituted name.
	 * @see Descriptor
	 */
	public JMethodInfo(final ConstantPool cp, final String methodname,
			final JMethodInfo src, final Map classnameMap) throws BadBytecode {
		this(cp);
		this.read(src, methodname, classnameMap);
	}

	/**
	 * Returns a string representation of the object.
	 */
	@Override
	public String toString() {
		return this.getName() + " " + this.getDescriptor();
	}

	/**
	 * Copies all constant pool items to a given new constant pool and replaces
	 * the original items with the new ones. This is used for garbage collecting
	 * the items of removed fields and methods.
	 * 
	 * @param cp
	 *            the destination
	 */
	void compact(final ConstantPool cp) {
		this.name = cp.addUtf8Info(this.getName());
		this.descriptor = cp.addUtf8Info(this.getDescriptor());
		this.attribute = AttributeInfo.copyAll(this.attribute, cp);
		this.constPool = cp;
	}

	void prune(final ConstantPool cp) {
		final ArrayList newAttributes = new ArrayList();

		AttributeInfo invisibleAnnotations = this
				.getAttribute(AnnotationsAttribute.invisibleTag);
		if (invisibleAnnotations != null) {
			invisibleAnnotations = invisibleAnnotations.copyAll(cp, null);
			newAttributes.add(invisibleAnnotations);
		}

		AttributeInfo visibleAnnotations = this
				.getAttribute(AnnotationsAttribute.visibleTag);
		if (visibleAnnotations != null) {
			visibleAnnotations = visibleAnnotations.copyAll(cp, null);
			newAttributes.add(visibleAnnotations);
		}

		AttributeInfo parameterInvisibleAnnotations = this
				.getAttribute(ParameterAnnotationsAttribute.invisibleTag);
		if (parameterInvisibleAnnotations != null) {
			parameterInvisibleAnnotations = parameterInvisibleAnnotations
					.copyAll(cp, null);
			newAttributes.add(parameterInvisibleAnnotations);
		}

		AttributeInfo parameterVisibleAnnotations = this
				.getAttribute(ParameterAnnotationsAttribute.visibleTag);
		if (parameterVisibleAnnotations != null) {
			parameterVisibleAnnotations = parameterVisibleAnnotations.copyAll(
					cp, null);
			newAttributes.add(parameterVisibleAnnotations);
		}

		final AnnotationDefaultAttribute defaultAttribute = (AnnotationDefaultAttribute) this
				.getAttribute(AnnotationDefaultAttribute.tag);
		if (defaultAttribute != null) {
			newAttributes.add(defaultAttribute);
		}

		final ExceptionsAttribute ea = this.getExceptionsAttribute();
		if (ea != null) {
			newAttributes.add(ea);
		}

		AttributeInfo signature = this.getAttribute(SignatureAttribute.tag);
		if (signature != null) {
			signature = signature.copyAll(cp, null);
			newAttributes.add(signature);
		}

		this.attribute = newAttributes;
		this.name = cp.addUtf8Info(this.getName());
		this.descriptor = cp.addUtf8Info(this.getDescriptor());
		this.constPool = cp;
	}

	/**
	 * Returns a method name.
	 */
	public String getName() {
		if (this.cachedName == null) {
			this.cachedName = this.constPool.getUtf8Info(this.name);
		}

		return this.cachedName;
	}

	/**
	 * Sets a method name.
	 */
	public void setName(final String newName) {
		this.name = this.constPool.addUtf8Info(newName);
		this.cachedName = newName;
	}

	/**
	 * Returns true if this is not a constructor or a class initializer (static
	 * initializer).
	 */
	public boolean isMethod() {
		final String n = this.getName();
		return !n.equals(nameInit) && !n.equals(nameClinit);
	}

	/**
	 * Returns a constant pool table used by this method.
	 */
	public ConstantPool getConstPool() {
		return this.constPool;
	}

	/**
	 * Returns true if this is a constructor.
	 */
	public boolean isConstructor() {
		return this.getName().equals(nameInit);
	}

	/**
	 * Returns true if this is a class initializer (static initializer).
	 */
	public boolean isStaticInitializer() {
		return this.getName().equals(nameClinit);
	}

	/**
	 * Returns access flags.
	 * 
	 * @see AccessFlag
	 */
	public int getAccessFlags() {
		return this.accessFlags;
	}

	/**
	 * Sets access flags.
	 * 
	 * @see AccessFlag
	 */
	public void setAccessFlags(final int acc) {
		this.accessFlags = acc;
	}

	/**
	 * Returns a method descriptor.
	 * 
	 * @see Descriptor
	 */
	public String getDescriptor() {
		return this.constPool.getUtf8Info(this.descriptor);
	}

	/**
	 * Sets a method descriptor.
	 * 
	 * @see Descriptor
	 */
	public void setDescriptor(final String desc) {
		if (!desc.equals(this.getDescriptor())) {
			this.descriptor = this.constPool.addUtf8Info(desc);
		}
	}

	/**
	 * Returns all the attributes. The returned <code>List</code> object is
	 * shared with this object. If you add a new attribute to the list, the
	 * attribute is also added to the method represented by this object. If you
	 * remove an attribute from the list, it is also removed from the method.
	 * 
	 * @return a list of <code>AttributeInfo</code> objects.
	 * @see AttributeInfo
	 */
	public List getAttributes() {
		if (this.attribute == null) {
			this.attribute = new ArrayList();
		}

		return this.attribute;
	}

	/**
	 * Returns the attribute with the specified name. If it is not found, this
	 * method returns null.
	 * 
	 * @param name
	 *            attribute name
	 * @return an <code>AttributeInfo</code> object or null.
	 * @see #getAttributes()
	 */
	public AttributeInfo getAttribute(final String name) {
		return AttributeInfo.lookup(this.attribute, name);
	}

	/**
	 * Appends an attribute. If there is already an attribute with the same
	 * name, the new one substitutes for it.
	 * 
	 * @see #getAttributes()
	 */
	public void addAttribute(final AttributeInfo info) {
		if (this.attribute == null) {
			this.attribute = new ArrayList();
		}

		AttributeInfo.remove(this.attribute, info.getName());
		this.attribute.add(info);
	}

	/**
	 * Returns an Exceptions attribute.
	 * 
	 * @return an Exceptions attribute or null if it is not specified.
	 */
	public ExceptionsAttribute getExceptionsAttribute() {
		final AttributeInfo info = AttributeInfo.lookup(this.attribute,
				ExceptionsAttribute.tag);
		return (ExceptionsAttribute) info;
	}

	/**
	 * Returns a Code attribute.
	 * 
	 * @return a Code attribute or null if it is not specified.
	 */
	public CodeAttribute getCodeAttribute() {
		final AttributeInfo info = AttributeInfo.lookup(this.attribute,
				CodeAttribute.tag);
		return (CodeAttribute) info;
	}

	/**
	 * Removes an Exception attribute.
	 */
	public void removeExceptionsAttribute() {
		AttributeInfo.remove(this.attribute, ExceptionsAttribute.tag);
	}

	/**
	 * Adds an Exception attribute.
	 * 
	 * <p>
	 * The added attribute must share the same constant pool table as this
	 * <code>method_info</code> structure.
	 */
	public void setExceptionsAttribute(final ExceptionsAttribute cattr) {
		this.removeExceptionsAttribute();
		if (this.attribute == null) {
			this.attribute = new ArrayList();
		}

		this.attribute.add(cattr);
	}

	/**
	 * Removes a Code attribute.
	 */
	public void removeCodeAttribute() {
		AttributeInfo.remove(this.attribute, CodeAttribute.tag);
	}

	/**
	 * Adds a Code attribute.
	 * 
	 * <p>
	 * The added attribute must share the same constant pool table as this
	 * <code>method_info</code> structure.
	 */
	public void setCodeAttribute(final CodeAttribute cattr) {
		this.removeCodeAttribute();
		if (this.attribute == null) {
			this.attribute = new ArrayList();
		}

		this.attribute.add(cattr);
	}

	/**
	 * Rebuilds a stack map table if the class file is for Java 6 or later. Java
	 * 5 or older Java VMs do not recognize a stack map table. If
	 * <code>doPreverify</code> is true, this method also rebuilds a stack map
	 * for J2ME (CLDC).
	 * 
	 * @param pool
	 *            used for making type hierarchy.
	 * @param cf
	 *            rebuild if this class file is for Java 6 or later.
	 * @see #rebuildStackMap(ClassPool)
	 * @see #rebuildStackMapForME(ClassPool)
	 * @since 3.6
	 */
	public void rebuildStackMapIf6(final ClassPool pool, final ClassFile cf)
			throws BadBytecode {
		if (cf.getMajorVersion() >= ClassFile.JAVA_6) {
			this.rebuildStackMap(pool);
		}

		if (doPreverify) {
			this.rebuildStackMapForME(pool);
		}
	}

	/**
	 * Rebuilds a stack map table. If no stack map table is included, a new one
	 * is created. If this <code>MethodInfo</code> does not include a code
	 * attribute, nothing happens.
	 * 
	 * @param pool
	 *            used for making type hierarchy.
	 * @see StackMapTable
	 * @since 3.6
	 */
	public void rebuildStackMap(final ClassPool pool) throws BadBytecode {
		final CodeAttribute ca = this.getCodeAttribute();
		if (ca != null) {
			final StackMapTable smt = MapMaker.make(pool, this);
			ca.setAttribute(smt);
		}
	}

	/**
	 * Rebuilds a stack map table for J2ME (CLDC). If no stack map table is
	 * included, a new one is created. If this <code>MethodInfo</code> does not
	 * include a code attribute, nothing happens.
	 * 
	 * @param pool
	 *            used for making type hierarchy.
	 * @see StackMapTable
	 * @since 3.12
	 */
	public void rebuildStackMapForME(final ClassPool pool) throws BadBytecode {
		final CodeAttribute ca = this.getCodeAttribute();
		if (ca != null) {
			final StackMap sm = MapMaker.make2(pool, this);
			ca.setAttribute(sm);
		}
	}

	/**
	 * Returns the line number of the source line corresponding to the specified
	 * bytecode contained in this method.
	 * 
	 * @param pos
	 *            the position of the bytecode (&gt;= 0). an index into the code
	 *            array.
	 * @return -1 if this information is not available.
	 */
	public int getLineNumber(final int pos) {
		final CodeAttribute ca = this.getCodeAttribute();
		if (ca == null) {
			return -1;
		}

		final LineNumberAttribute ainfo = ca
				.getAttribute(LineNumberAttribute.tag);
		if (ainfo == null) {
			return -1;
		}

		return ainfo.toLineNumber(pos);
	}

	/**
	 * Changes a super constructor called by this constructor.
	 * 
	 * <p>
	 * This method modifies a call to <code>super()</code>, which should be at
	 * the head of a constructor body, so that a constructor in a different
	 * super class is called. This method does not change actual parameters.
	 * Hence the new super class must have a constructor with the same signature
	 * as the original one.
	 * 
	 * <p>
	 * This method should be called when the super class of the class declaring
	 * this method is changed.
	 * 
	 * <p>
	 * This method does not perform anything unless this <code>MethodInfo</code>
	 * represents a constructor.
	 * 
	 * @param superclass
	 *            the new super class
	 */
	public void setSuperclass(final String superclass) throws BadBytecode {
		if (!this.isConstructor()) {
			return;
		}

		final CodeAttribute ca = this.getCodeAttribute();
		final byte[] code = ca.getCode();
		final CodeIterator iterator = ca.iterator();
		final int pos = iterator.skipSuperConstructor();
		if (pos >= 0) { // not this()
			final ConstantPool cp = this.constPool;
			final int mref = ByteArray.readU16bit(code, pos + 1);
			final int nt = cp.getMethodrefNameAndType(mref);
			final int sc = cp.addClassInfo(superclass);
			final int mref2 = cp.addMethodrefInfo(sc, nt);
			ByteArray.write16bit(mref2, code, pos + 1);
		}
	}

	private void read(final MethodInfo src, final String methodname,
			final Map classnames) throws BadBytecode {
		final ConstantPool destCp = this.constPool;
		this.accessFlags = src.accessFlags;
		this.name = destCp.addUtf8Info(methodname);
		this.cachedName = methodname;
		final ConstantPool srcCp = src.constantPool;
		final String desc = srcCp.getUtf8Info(src.descriptor);
		final String desc2 = Descriptor.rename(desc, classnames);
		this.descriptor = destCp.addUtf8Info(desc2);

		this.attribute = new ArrayList();
		final ExceptionsAttribute eattr = src.getExceptionsAttribute();
		if (eattr != null) {
			this.attribute.add(eattr.copy(destCp, classnames));
		}

		final CodeAttribute cattr = src.getCodeAttribute();
		if (cattr != null) {
			this.attribute.add(cattr.copy(destCp, classnames));
		}
	}

	private void read(final DataInputStream in) throws IOException {
		this.accessFlags = in.readUnsignedShort();
		this.name = in.readUnsignedShort();
		this.descriptor = in.readUnsignedShort();
		final int n = in.readUnsignedShort();
		this.attribute = new ArrayList();
		for (int i = 0; i < n; ++i) {
			this.attribute.add(AttributeInfo.read(this.constPool, in));
		}
	}

	void write(final DataOutputStream out) throws IOException {
		out.writeShort(this.accessFlags);
		out.writeShort(this.name);
		out.writeShort(this.descriptor);

		if (this.attribute == null) {
			out.writeShort(0);
		} else {
			out.writeShort(this.attribute.size());
			AttributeInfo.writeAll(this.attribute, out);
		}
	}
}
